package com.feonix.blockchain.dao;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.feonix.blockchain.pojo.Block;
import com.feonix.blockchain.pojo.Transaction;
import com.feonix.blockchain.util.Encrypt;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class BlockChain {
    /**
     * 存储区块链
     */
    private List<Block> chain;
    /**
     * 当前交易信息列表
     */
    private List<Transaction> currentTransactions;
    /**
     * 用于存储网络节点的集合
     */
    private Set<String> nodes;

    private static BlockChain blockChain = null;

    private BlockChain() {
        // 初始化区块链
        this.chain = new ArrayList<Block>();
        // 初始化当前的交易信息列表
        this.currentTransactions = new ArrayList<Transaction>();
        // 初始化网络节点集合
        this.nodes = new HashSet<String>();

        // 创建创世区块
        newBlock(100, "0");
    }

    // 创建单例对象
    public static BlockChain getInstance() {
        if (blockChain == null) {
            synchronized (BlockChain.class) {
                if (blockChain == null) {
                    blockChain = new BlockChain();
                }
            }
        }
        return blockChain;
    }

    public List<Block> getChain() {
        return chain;
    }

    public void setChain(List<Block> chain) {
        this.chain = chain;
    }

    public List<Transaction> getCurrentTransactions() {
        return currentTransactions;
    }

    public void setCurrentTransactions(List<Transaction> currentTransactions) {
        this.currentTransactions = currentTransactions;
    }

    public Set<String> getNodes() {
        return nodes;
    }

    /**
     * 获取到区块链中最后一个区块
     *
     * @return
     */
    public Block getLastBlock() {
        return getChain().get(getChain().size() - 1);
    }

    /**
     * 在区块链上新建一个区块
     *
     * @param proof         新区块的工作量证明
     * @param previous_hash 上一个区块的hash值
     * @return 返回新建的区块
     */
    public Block newBlock(long proof, String previous_hash) {
        Block block = new Block();
        block.setIndex(getChain().size() + 1);
        block.setTimestamp(System.currentTimeMillis());
        block.setTransactions(getCurrentTransactions());
        block.setProof(proof);
        block.setPrevious_hash(previous_hash != null ? previous_hash : hash(getLastBlock()));

        // 重置当前的交易信息列表
        setCurrentTransactions(new ArrayList<Transaction>());

        getChain().add(block);

        return block;
    }

    /**
     * 生成新交易信息，信息将加入到下一个待挖的区块中
     *
     * @param sender    发送方的地址
     * @param recipient 接收方的地址
     * @param amount    交易数量
     * @return 返回该交易事务的块的索引
     */
    public int newTransactions(String sender, String recipient, long amount) {

        Transaction transaction = new Transaction();
        transaction.setSender(sender);
        transaction.setRecipient(recipient);
        transaction.setAmount(amount);

        getCurrentTransactions().add(transaction);

        return getLastBlock().getIndex() + 1;
    }

    /**
     * 简单的工作量证明:
     * - 查找一个 p' 使得 hash(pp') 以4个0开头
     * - p 是上一个块的证明, p' 是当前的证明
     *
     * @param last_proof 上一个块的证明
     * @return
     */
    public long proofOfWork(long last_proof) {
        long proof = 0;
        while (!validProof(last_proof, proof)) {
            proof += 1;
        }
        return proof;
    }

    /**
     * 验证证明: 是否hash(last_proof, proof)以4个0开头?
     *
     * @param last_proof 上一个块的证明
     * @param proof      当前的证明
     * @return 以4个0开头返回true，否则返回false
     */
    public boolean validProof(long last_proof, long proof) {
        String guess = last_proof + "" + proof;
        String guess_hash = Encrypt.getSHA256(guess);
        return guess_hash.startsWith("0000");
    }

    /**
     * 注册网络节点
     *
     * @param address 节点地址
     * @throws MalformedURLException
     */
    public void registerNode(String address) throws MalformedURLException {
        URL url = new URL(address);
        String node = String.format("%s:%s", url.getHost(), url.getPort() == -1 ? url.getDefaultPort() : url.getPort());
        nodes.add(node);
    }

    /**
     * 检查是否是有效链，遍历每个区块验证hash和proof，来确定一个给定的区块链是否有效
     *
     * @param chain
     * @return
     */
    public boolean validChain(List<Block> chain) {
        Block lastBlock = chain.get(0);
        int currentIndex = 1;
        while (currentIndex < chain.size()) {
            Block block = chain.get(currentIndex);

            System.out.println(lastBlock.toString());
            System.out.println(block.toString());
            System.out.println("\n-------------------------\n");

            // 检查block的hash是否正确
            if (block.getPrevious_hash() == null || !block.getPrevious_hash().equals(hash(lastBlock))) {
                return false;
            }

            lastBlock = block;
            currentIndex++;
        }
        return true;
    }

    /**
     * 共识算法解决冲突，使用网络中最长的链.
     * 遍历所有的邻居节点，并用上一个方法检查链的有效性，
     * 如果发现有效更长链，就替换掉自己的链
     *
     * @return 如果链被取代返回true, 否则返回false
     * @throws IOException
     */
    public boolean resolveConflicts() throws IOException {
        List<Block> newChain = null;
        // 寻找最长的区块链
        long maxLength = this.chain.size();

        // 获取并验证网络中的所有节点的区块链
        for (String node : nodes) {
            URL url = new URL("http://" + node + "/wallet/chain");
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            connection.connect();

            if (connection.getResponseCode() == 200) {
                BufferedReader bufferedReader = new BufferedReader(
                        new InputStreamReader(connection.getInputStream(), "utf-8"));
                StringBuffer responseData = new StringBuffer();
                String response = null;
                while ((response = bufferedReader.readLine()) != null) {
                    responseData.append(response);
                }
                bufferedReader.close();

                System.out.println("responseData ------> " + responseData.toString());

                JSONObject jsonData = JSON.parseObject(responseData.toString());
                long length = jsonData.getLong("length");
                List<Block> chain = jsonData.getJSONArray("chain").toJavaList(Block.class);

                // 检查长度是否长，链是否有效
                if (length > maxLength && validChain(chain)) {
                    maxLength = length;
                    newChain = chain;
                }
            }

        }
        // 如果发现一个新的有效链比我们的长，就替换当前的链
        if (newChain != null) {
            this.chain = newChain;
            return true;
        }
        return false;
    }

    /**
     * 生成区块的 SHA-256格式的 hash值
     *
     * @param block 区块
     * @return 返回该区块的hash
     */
    public static String hash(Block block) {
        return Encrypt.getSHA256(JSON.toJSONString(block));
    }
}
